你想在場景裡放 幾百、幾千 棵樹、石頭、子彈殼嗎?一個一個畫(Draw Call)會把 CPU 累死。
GPU Instancing 的想法超單純:同一個網格+同一個材質,一次下命令,請顯示卡自己「複製很多份」來畫;差異(顏色、大小、位置…)交給每一份「分身」各自帶的資料。
成果:Draw Call 大幅下降,CPU 輕鬆很多,FPS 更穩。
口訣:東西要長得一樣(mesh & material),只允許少量可變(per-instance)屬性。
能變(per-instance)
不能變(共享)
要不同貼圖?常見做法:圖集(Atlas)或 Array Texture,然後用「每實例索引」挑片段;這算進階題,今天先不展開。
成效可用 Stats 與 Frame Debugger 觀察:
Batches
/SetPass Calls
應該會下降。
但自動合批條件很多(光照/陰影/Lightmap/關鍵字不同都會拆批),所以實務上我們常自己寫一個「保證支援 Instancing」的 Shader + 用程式一次繪製。
重點:使用 Unity 的 Instancing 宏,把「每實例顏色」宣告成 Instanced Property。
之後我們會用Graphics.DrawMeshInstanced
一口氣畫上千個、每個顏色都不同。
Shader "CustomLearning/Unlit_InstancedColor"
{
Properties
{
_BaseMap ("Base (sRGB)", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Pass
{
Cull Back
ZWrite On
Blend One Zero
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#pragma multi_compile_instancing // ★ 開啟 Instancing 變體
#include "UnityCG.cginc"
sampler2D _BaseMap; float4 _BaseMap_ST;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // ★ 頂點輸入帶 Instance ID
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // ★ 透傳 Instance ID
};
// ★ 宣告每實例屬性:這裡用顏色
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color) // 每實例的 Tint
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o); // 傳到 frag
// 每實例的物件→世界矩陣由 Unity 自動提供
float4 posWS = mul(unity_ObjectToWorld, v.vertex);
o.pos = mul(UNITY_MATRIX_VP, posWS);
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
fixed4 tex = tex2D(_BaseMap, i.uv);
float4 tint = UNITY_ACCESS_INSTANCED_PROP(Props, _Color); // ★ 取出每實例顏色
return tex * tint;
}
ENDCG
}
}
}
你剛學到:
#pragma multi_compile_instancing
開啟 instancing 支援。UNITY_INSTANCING_BUFFER
區塊裡用 UNITY_DEFINE_INSTANCED_PROP
宣告「每實例」變數。UNITY_SETUP/TRANSFER/ACCESS_INSTANCE_ID
宏讓 shader 知道正在處理哪一份分身。unity_ObjectToWorld
/ UNITY_MATRIX_VP
,Unity 會給每實例不同的矩陣。Graphics.DrawMeshInstanced
每次最多 1023 份(API 限制),要更多就分批呼叫。
我們會:建一個 2D 陣列的方格、每格一個 Matrix4x4 + 一個顏色,一個材質、幾個批次全畫出來。
using System.Collections.Generic;
using UnityEngine;
public class InstancingDemo : MonoBehaviour
{
public Mesh mesh; // 指向 Cube, Sphere, 或你自己的 Mesh
public Material instancedMaterial; // 使用上面那個 "Unlit_InstancedColor" Shader
public int countPerAxis = 32; // 32x32 = 1024 個
public float spacing = 2f;
const int BatchSize = 1023; // API 限制
readonly List<Matrix4x4[]> _matBatches = new();
readonly List<MaterialPropertyBlock> _mpbBatches = new();
void Start()
{
int total = countPerAxis * countPerAxis;
int done = 0;
while (done < total)
{
int n = Mathf.Min(BatchSize, total - done);
var mats = new Matrix4x4[n];
var colors = new Vector4[n];
for (int i = 0; i < n; i++)
{
int idx = done + i;
int x = idx % countPerAxis;
int y = idx / countPerAxis;
Vector3 pos = new(
(x - countPerAxis / 2f) * spacing,
0f,
(y - countPerAxis / 2f) * spacing);
mats[i] = Matrix4x4.TRS(pos, Quaternion.identity, Vector3.one);
Color c = Color.HSVToRGB((float)idx / total, 0.8f, 1f);
colors[i] = new Vector4(c.r, c.g, c.b, 1f);
}
var mpb = new MaterialPropertyBlock();
mpb.SetVectorArray("_Color", colors); // ★ 把每實例顏色陣列塞進去
_matBatches.Add(mats);
_mpbBatches.Add(mpb);
done += n;
}
}
void Update()
{
for (int b = 0; b < _matBatches.Count; b++)
{
Graphics.DrawMeshInstanced(
mesh,
0,
instancedMaterial,
_matBatches[b],
_matBatches[b].Length,
_mpbBatches[b],
UnityEngine.Rendering.ShadowCastingMode.Off, // 先關影子,之後再試
false,
0, null,
UnityEngine.Rendering.LightProbeUsage.Off);
}
}
}
成果:
你剛學到:
_Color
。如果你有 1000 個 MeshRenderer
物件、共享同一個材質,而且材質支援 Instancing,Unity 有機會 自動把它們合在同一個 instanced draw 裡。
但只要中間出現不同關鍵字、Lightmap 索引、Shadow 設定、Render Queue…就會被拆掉。
**實務建議:**要穩定可控、大量物件,用 Graphics.DrawMeshInstanced
。
Frame Debugger:Window > Analysis > Frame Debugger
,啟用看看是不是出現 Draw Mesh (Instanced)
。
Stats 視窗:看 Batches
/ SetPass calls
是否下降,Triangles
會差不多(因為還是畫那麼多面)。
材質 Inspector:確認 Enable GPU Instancing 有勾(大多 shader 需要)。
顏色沒生效?
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
。SetVectorArray("_Color", …)
的陣列長度要跟本批次實例數一致。沒合批?
DrawMeshInstanced
對整批做裁切;若一批全部不在視錐外就不畫,但部分在內時整批都會送 GPU。把格子分區、離鏡頭遠的分成另一批。DrawMeshInstancedIndirect + Compute Shader
(之後再玩)。countPerAxis
翻倍,看 Batches
幾乎沒變,但 Triangles 翻倍。i%2
控制顏色,驗證每實例屬性真的各自生效。Update
裡把某個批次的矩陣乘上 Quaternion.Euler
,觀察 GPU 負載與 CPU 穩定度。instancedMaterialA
,另一半用 B,觀察 Batches
變多(證明「材質不同就拆批」)。ShadowCastingMode.On
打開,再觀察批次變化與效能差異。GPU Instancing = 讓 GPU 一口氣畫同一套網格+材質的很多分身,
透過「每實例矩陣+少量屬性」來做差異化。
寫一個支援 instancing 的 shader,再用Graphics.DrawMeshInstanced
投出幾百幾千個物件,你就把 CPU 從「瘋狂下命令」中解放出來了。下一步想衝更大,才需要DrawMeshInstancedIndirect
與 Compute 做可見性/LOD。祝你場景滿滿、FPS 穩穩!